/*
 Copyright (c) 2013, 2021, Oracle and/or its affiliates. All rights
 reserved.
 
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License, version 2.0,
 as published by the Free Software Foundation.

 This program is also distributed with certain software (including
 but not limited to OpenSSL) that is licensed under separate terms,
 as designated in a particular file or component or in included license
 documentation.  The authors of MySQL hereby grant you an additional
 permission to link the program and your derivative works with the
 separately licensed software that they have included with MySQL.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License, version 2.0, for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 02110-1301  USA
 */

"use strict";

var udebug = unified_debug.getLogger("lib/read_write.js");
var mysql = require("mysql");
var util = require("util");

/** This is the test for data conversions. It reads using one implementation
 * (either a mysql-js adapter or a sql driver) and writes using an
 * implementation (either a mysql-js adapter or a sql driver) and
 * then compares the results. 
 */

/** Create a driver for sql connections.
 */
function SQLDriver(connectionProperties) {
  /* Translate our properties to the driver's */
  this.props = {};
  if(connectionProperties.mysql_socket) {
    this.props.SocketPath = connectionProperties.mysql_socket;
  } else {
    this.props.host = connectionProperties.mysql_host;
    this.props.port = connectionProperties.mysql_port;
  }
  if(connectionProperties.mysql_user) {
    this.props.user = connectionProperties.mysql_user;
  }
  if(connectionProperties.mysql_password) {
    this.props.password = connectionProperties.mysql_password;
  }
  this.props.database = connectionProperties.database;
  this.props.debug = connectionProperties.mysql_debug;
  udebug.log('SQLDriver using properties: ', util.inspect(this.props));
};

SQLDriver.prototype.connect = function(callback) {
  this.connection = mysql.createConnection(this.props);
  this.connection.connect(callback);
};

SQLDriver.prototype.remove = function(tableMapping, element, callback) {
  // create delete statement and values object
  var statement = 'delete from ' + tableMapping.table + ' where ';
  var keys = [];
  var separator = '';
  tableMapping.pkFields.forEach(function(pkField) {
    statement += separator;
    statement += pkField.columnName;
    statement += ' = ?';
    
    keys.push(element[pkField.fieldName]);
    separator = ' and ';
  });
  udebug.log_detail(statement, ' with keys: ', keys);
  this.connection.query(statement, keys, callback);
};

SQLDriver.prototype.insert = function(tableMapping, element, callback, rw, index) {
  function onInsert(err) {
    callback(err, rw, index);
  }

  var insertClause = 'insert into ' + tableMapping.table + ' (';
  var valueClause = ') values (';
  var values = [];
  var value;
  var separator = '';
  tableMapping.fields.forEach(function(field) {
    value = element[field.fieldName];
    // only send defined values (including explicit nulls)
    if (typeof(value) !== 'undefined') {
      insertClause += separator;
      insertClause += field.columnName;
      valueClause += separator;
      valueClause += '? ';
      separator = ', ';
      values.push(value);
    }
  });
  insertClause += valueClause;
  insertClause += ')';
  udebug.log_detail(insertClause, ' with values: ', values);
  this.connection.query(insertClause, values, onInsert);
};

SQLDriver.prototype.select = function(tableMapping, element, callback, rw, index) {
  function onSelect(err, results) {
    callback(err, results[0], rw, index);
  }

  var selectClause = 'select ';
  var whereClause = ' from ' + tableMapping.table + ' where ';
  var keys = [];
  var separator = '';
  // append ", columnName as fieldName"
  tableMapping.fields.forEach(function(field) {
    selectClause += separator;
    selectClause += field.columnName;
    selectClause += ' as ';
    selectClause += field.fieldName;
    separator = ', ';
  });
  separator = '';
  // append ", pkField = ?"
  tableMapping.pkFields.forEach(function(pkField) {
    whereClause += separator;
    whereClause += pkField.columnName;
    whereClause += ' = ?';
    
    keys.push(element[pkField.fieldName]);
    separator = ' and ';
  });
  selectClause += whereClause;
  udebug.log_detail(selectClause, ' with keys: ', keys);
  this.connection.query(selectClause, keys, onSelect);
};

/** Augment the table mapping with additional metadata to facilitate creating
 * SQL from a domain object. Primary key columns will be identified with the
 * column name and corresponding field name.
 */
function augmentTableMapping(tableMapping, tableMetadata) {
  // create list of primary key columns
  var pkColumns = [];
  tableMetadata.columns.forEach(function(column) {
    if (column.isInPrimaryKey) {
      pkColumns.push(column.name);
    }
  });
  tableMapping.pkFields = [];
  tableMapping.fields.forEach(function(field) {
    if (pkColumns.indexOf(field.columnName) !== -1) {
      tableMapping.pkFields.push({fieldName: field.fieldName, columnName: field.columnName});
    }
  });
  udebug.log('augmentTableMapping ', tableMapping.pkFields);
};

/** Construct a new ReadWrite for a write implementation,
 * a read implementation, and test data.
 * The write implementation can be either an adapter or a driver.
 * The read implementation can be either an adapter or a driver.
 * The test data can be a domain object or a plain object.
 * 
 */
var ReadWrite = function(testCase, tableNameOrConstructor, data, session, SQLDriver) {
  this.testCase = testCase;
  this.tableNameOrConstructor = tableNameOrConstructor;
  this.data = data;
  this.session = session;
  this.SQLDriver = SQLDriver;
  this.numberChecked = 0;
  this.numberRemoved = 0;
};

/** Set up the structure for running the tests:
 * Make sure the data object is an array.
 * Get the table tableMapping for the domain object or table.
 * Connect to the sql driver.
 * Get the tableMetadata for the domain object or table.
 * Delete all rows corresponding to the data.
 */
ReadWrite.prototype.setUp = function(callback) {
  var rw = this;

  function onRemove(err) {
    if (err) {
      // delete will return 02000 if no row deleted
      if (err.sqlstate !== '02000') {
        rw.testCase.appendErrorMessage(util.inspect(err));
      }
    }
    if (++rw.numberRemoved == rw.data.length) {
      // continue if no errors
      if (rw.testCase.hasNoErrors()) {
        callback();
      } else {
        rw.testCase.failOnError();
      }
    }
  }

  function onTableMetadata(err, tableMetadata) {
    if (err) {
      rw.testCase.appendErrorMessage('sql driver failed to connect.' + err);
      rw.sqlDriver = undefined;
      rw.testCase.fail();
    } else {
      rw.tableMetadata = tableMetadata;
      augmentTableMapping(rw.tableMapping, rw.tableMetadata);
      // delete all rows corresponding to data
      rw.data.forEach(function(element) {
        rw.remove(element, onRemove);
      });
    }
  }

  function onConnect(err) {
    if (err) {
      rw.testCase.appendErrorMessage('sql driver failed to connect.' + err);
      rw.sqlDriver = undefined;
      rw.testCase.fail();
    } else {
      // get metadata for table
      rw.session.getTableMetadata(rw.tableMapping.database, rw.tableMapping.table, onTableMetadata);
    }
  }

  function onTableMapping(err, tableMapping) {
    if (err) {
      rw.testCase.appendErrorMessage('failed to get table mapping.' + err);
      rw.testCase.fail();
    } else {
      rw.tableMapping = tableMapping;
      udebug.log('got mapping for ', rw.tableMapping.table);
      // set up sql driver
      if (rw.sqlDriver === undefined) {
        rw.sqlDriver = new SQLDriver(global.test_conn_properties);
        rw.sqlDriver.connect(onConnect);
      } else {
        console.log('already set up sql driver')
        // already set up sql driver
        onConnect(null);
      }
    }
  }
  // setUp starts here
  if (!Array.isArray(rw.data)) {
    rw.data = [rw.data];
  }
  rw.session.getMapping(rw.tableNameOrConstructor, onTableMapping);
};

ReadWrite.prototype.incrementCheckCountAndExit = function() {
  if (++this.numberChecked == this.data.length) {
    this.testCase.failOnError();
  }
};

ReadWrite.prototype.removeAdapter = function(element, onRemove) {
  this.session.remove(this.tableNameOrConstructor, element, onRemove);
};

ReadWrite.prototype.removeSQL = function(element, onRemove) {
  this.sqlDriver.remove(this.tableMapping, element, onRemove);
};

ReadWrite.prototype.checkResult = function(err, result, rw, index) {
  udebug.log('checkResult', err, rw.data, result, index);
  var rw = rw;
  if (err) {
    rw.testCase.appendErrorMessage('checkResult err on read: ', err);
  } else {
    if (typeof(rw.tableNameOrConstructor) === 'string') {
      // table name; check properties in data against properties in result
      var x;
      var data = rw.data[index];
      for (x in data) {
        if (data.hasOwnProperty(x)) {
          rw.testCase.errorIfNotEqual('mismatch in ' + x, data[x], result[x]);
        }
      }
    } else {
      // constructor
    }
  }
  rw.incrementCheckCountAndExit();
};

/** Write data to the table or constructor. 
 * @param callback (err, readWrite)
 * @return nothing
 */
ReadWrite.prototype.writeAdapter = function(callback) {
  var rw = this;

  // write all elements of data
  var index = 0;
  rw.data.forEach(function(element) {
    rw.session.persist(rw.tableNameOrConstructor, element, callback, rw, index++);
  });
};

ReadWrite.prototype.writeSQL = function(callback) {
  var rw = this;
  var index = 0;
  rw.data.forEach(function(element) {
    rw.sqlDriver.insert(rw.tableMapping, element, callback, rw, index++);
  });
};

ReadWrite.prototype.readAdapter = function(err, rw, index) {
  if (err) {
    rw.testCase.appendErrorMessage('readAdapter err on write: ' + err);
    rw.incrementCheckCountAndExit();    
  } else {
    rw.session.find(rw.tableNameOrConstructor, rw.data[index],
        rw.checkResult, rw, index);
  }
};

ReadWrite.prototype.readSQL = function(err, rw, index) {
  if (err) {
    rw.testCase.appendErrorMessage('readSQL err on write: ' + err);
    rw.incrementCheckCountAndExit();    
  } else {
    rw.sqlDriver.select(rw.tableMapping, rw.data[index],
        rw.checkResult, rw, index);
  }
};

ReadWrite.prototype.writeAdapterReadAdapter = function() {
  var rw = this;
  rw.remove = rw.removeAdapter;
  function run() {
    rw.writeAdapter(rw.readAdapter);
  }
  rw.setUp(run);
};

ReadWrite.prototype.writeAdapterReadSQL = function() {
  var rw = this;
  rw.remove = rw.removeAdapter;
  function run() {
    rw.writeAdapter(rw.readSQL);
  }
  rw.setUp(run);
};

ReadWrite.prototype.writeSQLReadAdapter = function() {
  var rw = this;
  rw.remove = rw.removeSQL;
  function run() {
    rw.writeSQL(rw.readAdapter);
  }
  rw.setUp(run);
};

ReadWrite.prototype.writeSQLReadSQL = function() {
  var rw = this;
  rw.remove = rw.removeSQL;
  function run() {
    rw.writeSQL(rw.readSQL);
  }
  rw.setUp(run);
};

exports.ReadWrite = ReadWrite;
